package org.hugoduncan.appam.servlet;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;

import org.apache.abdera.model.Collection;
import org.apache.abdera.model.Document;
import org.apache.abdera.model.Element;
import org.apache.abdera.model.Entry;
import org.apache.abdera.model.Feed;
import org.apache.abdera.model.Service;
import org.apache.abdera.parser.ParseException;
import org.apache.abdera.parser.Parser;
import org.apache.abdera.protocol.client.Client;
import org.apache.abdera.protocol.client.CommonsClient;
import org.apache.abdera.protocol.client.RequestOptions;
import org.apache.abdera.protocol.client.Response;
import org.apache.abdera.xpath.XPath;
import org.apache.abdera.xpath.XPathException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.commons.io.IOUtils;

import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;


public class TestApplicationServlet extends TestCase
{
    private static final String IMAGE_PNG = "image/png";
    private static final String APPLICATION_ATOM_XML = "application/atom+xml";
    private static final Log log=
        LogFactory.getLog(TestApplicationServlet.class);

    
    /**
     * @return the suite of tests being tested
     */
    public static Test suite()
    {
        return new ContainerTestSetup(new TestSuite( TestApplicationServlet.class ));
    }

    @Override
    public void setUp() throws Exception
    {
        super.setUp();

        // configure xpath
        Map<String, String> ns=XPath.INSTANCE.getDefaultNamespaces();
        if (!ns.containsKey("appam"))
        {
            ns.put("appam", "http://hugoduncan.org/atom/appam#");
            ns.put("atom", "http://www.w3.org/2005/Atom");
            XPath.INSTANCE.setDefaultNamespaces(ns);
        }


        File file=new File("c:/tmp/c1.atom");
        file.delete();
        file=new File("c:/tmp/c2.atom");
        file.delete();
    }
    
    /*
    public void testFileSystemIntrospection()
    {
        HttpClient client=new HttpClient();
        HttpMethod get = new GetMethod("http://localhost:8080/testserver/fs");
        String response="";
        try {
            client.executeMethod(get);
            response=get.getResponseBodyAsString();
            assertEquals(200,get.getStatusCode());
            Header contentType=get.getResponseHeader("Content-Type");
            assertEquals("application/atomserv+xml;charset=utf-8",
                          contentType.getValue());
        } catch (HttpException e) {
            fail("unexpected exception: "+e.getMessage());
        } catch (IOException e) {
            fail("unexpected exception: "+e.getMessage());
        }
        finally {
            get.releaseConnection();
        }

        log.info(response);
        assertTrue(response.contains("FS Workspace"));
        assertTrue(response.contains("Main FS Collection"));
    }

    @SuppressWarnings("unchecked")
    public void testFileSystemCollections()
    {
        HttpClient client=new HttpClient();
        String serviceURL="http://localhost:8080/testserver/fs";
        HttpMethod get = new GetMethod(serviceURL);
        try {
            client.executeMethod(get);
            assertEquals(200,get.getStatusCode());

            Document<Service> service=Parser.INSTANCE.parse(
                    get.getResponseBodyAsStream(),
                    new java.net.URI(serviceURL));

            // Select all entries from the document
            List<Collection> entries =
                XPath.INSTANCE.selectNodes("//app:collection", service);

            for (Collection collection : entries)
            {
                URI uri=null;
                if (collection.getBaseUri()!=null)
                {
                    String base=collection.getBaseUri().toString();
                    String href=collection.getHref().toString();
                    log.info("Base : "+base);
                    log.info("Href : "+href);
                    uri=new URI(base+href,true);
                }
                else
                {
                    uri=new URI(collection.getHref().toString(),true);
                }
                log.info("Feed at : "+uri.toString());

                // Read the feed for the collection
                HttpMethod getFeed = new GetMethod(uri.toString());
                try {
                    client.executeMethod(getFeed);
                    assertEquals(200,getFeed.getStatusCode());
                }
                finally {
                    getFeed.releaseConnection();
                }

            }
        } catch (HttpException e) {
            fail("unexpected exception: "+e.getMessage());
        } catch (IOException e) {
            fail("unexpected exception: "+e.getMessage());
        } catch (ParseException e) {
            fail("unexpected exception: "+e.getMessage());
        } catch (URISyntaxException e) {
            fail("unexpected exception: "+e.getMessage());
        } catch (XPathException e) {
            fail("unexpected exception: "+e.getMessage());
        }
        finally {
            get.releaseConnection();
        }
    }
*/

    @SuppressWarnings("unchecked")
    public void testPost()
    {
        Client client=new CommonsClient();

        String serviceURL="http://localhost:8080/testserver/fs";
        Service service=getService(client,serviceURL);
        debugWrite(service.getDocument());

        String feedURL="http://localhost:8080/testserver/fsc";
        String entryBaseURL="http://localhost:8080/testserver";
        
        Entry entry=null;
        try {
            entry=loadElement("/entry.atom",Entry.class);
        } catch (IOException e1) {
            fail("Failed loading test resource"+e1.getMessage());
            return;
        }

        log.debug("Entry to send to server");
        debugWrite(entry.getDocument());

        // Post the entry
        Map<Integer,Object> reply=postEntry(client, feedURL, entry);
        String location=(String) reply.get(LOCATION);
        Entry replyEntry=(Entry) reply.get(ENTRY);
        URI entryId=null;

        if (replyEntry!=null)
        {
            log.debug("Reply from server");
            debugWrite(replyEntry.getDocument());

            try {
                entryId=replyEntry.getId();
            } catch (URISyntaxException e) {
                fail("Id not valid "+e.getMessage());
            }
            assertNotNull(entryId);

            assertNotNull(replyEntry.getSummary());
            assertEquals(normaliseWhitespace(entry.getContent()),
                         normaliseWhitespace(replyEntry.getContent()));
            String editLocation;
                editLocation = getEntryEditLink(replyEntry);
                assertEquals(location, entryBaseURL+editLocation);

            Feed feed=getFeed(client,feedURL);
            checkFeedEntry(feed, 0, replyEntry);
        }

        if (location!=null)
        {
            // get the entry
            Entry getEntry=getEntry(client, location);
            assertNotNull(entry);
            try {
                assertEquals(replyEntry.getId(),getEntry.getId());
            } catch (URISyntaxException e) {
                fail("invalid uri");
            }
            assertEquals(normaliseWhitespace(entry.getContent()),
                         normaliseWhitespace(replyEntry.getContent()));

            // edit the entry to the same content
            putEntry(client,location, entry);
            Feed feed=getFeed(client,feedURL);

            checkFeedEntry(feed, 0, getEntry);

            // delete the entry
            deleteEntry(client,location);
            feed=getFeed(client,feedURL);
            assertTrue(feed.getEntries()==null||feed.getEntries().size()==0);
        }

    }

    @SuppressWarnings("unchecked")
    public void testPostImage()
    {
        Client client=new CommonsClient();

        log.debug("testPostImage");

        String serviceURL="http://localhost:8080/testserver/fs";
        Service service=getService(client,serviceURL);
        debugWrite(service.getDocument());

        Collection collection=
            service.getWorkspaces().get(0).getCollections().get(1);
        assertTrue(collection.accepts(IMAGE_PNG)||collection.accepts("image/*"));

        String feedURL="http://localhost:8080/testserver/fsi";
        Feed feed=getFeed(client, feedURL);

        InputStream is=TestApplicationServlet.class.getResourceAsStream("/Lme.png");

        // Post the entry
        Map<Integer,Object> reply=postImage(client, feedURL, is, IMAGE_PNG);
        String location=(String) reply.get(LOCATION);
        log.debug("Location:"+location);
        Entry replyEntry=(Entry) reply.get(ENTRY);
        URI entryId=null;

        if (replyEntry!=null)
        {
            log.debug("Reply from server");
            debugWrite(replyEntry.getDocument());

            try {
                entryId=replyEntry.getId();
            } catch (URISyntaxException e) {
                fail("Id not valid "+e.getMessage());
            }
            assertNotNull(entryId);

            assertNotNull(replyEntry.getSummary());
            try {
                assertNotNull(replyEntry.getContentElement().getSrc());
            } catch (URISyntaxException e1) {
                fail(e1.getMessage());
            }
            String editLocation = getEntryEditLink(replyEntry);
            assertTrue(location.endsWith(editLocation));
            String editMediaLocation=getEntryEditMediaLink(replyEntry);
            try {
                assertEquals(editMediaLocation,
                        replyEntry.getContentElement().getSrc().toString());
            } catch (URISyntaxException e) {
                fail(e.getMessage());
            }


            feed=getFeed(client,feedURL);
            checkFeedEntry(feed, 0, replyEntry);
        }

        if (location!=null)
        {
            // get the entry
            Entry getEntry=getEntry(client, location);
            assertNotNull(getEntry);
            try {
                assertEquals(replyEntry.getId(),getEntry.getId());
            } catch (URISyntaxException e) {
                fail("invalid uri");
            }
            try {
                assertEquals(getEntry.getContentElement().getSrc(),
                             replyEntry.getContentElement().getSrc());
            } catch (URISyntaxException e) {
                fail(e.getMessage());
            }

            // edit the entry to the same content
            is=TestApplicationServlet.class.getResourceAsStream("/Lme.png");

            putImage(client,location, is, IMAGE_PNG);
            feed=getFeed(client,feedURL);

            checkFeedEntry(feed, 0, getEntry);

            // delete the entry
            deleteEntry(client,location);
            feed=getFeed(client,feedURL);
            assertTrue(feed.getEntries()==null||feed.getEntries().size()==0);
        }

    }

    private String normaliseWhitespace(String arg)
    {
        arg=arg.replaceAll(">\\s+<", "><");
        arg=arg.replaceAll("\\s+$", "");
        arg=arg.replaceAll("^\\s+", "");
        return arg;
    }

    private void checkFeedEntry(Feed feed, int entryIndex, Entry entry)
    {
        assertNotNull(entry);
        assertNotNull(entry.getContent());

        Entry feedEntry=feed.getEntries().get(entryIndex);
        assertNotNull(feedEntry);
        assertNotNull(feedEntry.getContent());
        
        assertEquals(normaliseWhitespace(entry.getContent().toString()),
                     normaliseWhitespace(feedEntry.getContent().toString()));
        try {
            assertEquals(entry.getId(),feedEntry.getId());
        } catch (URISyntaxException e) {
            fail("Comparisons of id failed : "+e.getMessage());
        }
        assertEquals(entry.getLink("edit").toString(),
                     feedEntry.getLink("edit").toString());
    }

/*
    private String getFeedEntryEditLink(Feed feed)
    {
        try {
            return XPath.INSTANCE.valueOf(
                "atom:entry/atom:link[@rel='edit']/@href", feed);
        } catch (XPathException e) {
            fail("Unable to get edit link "+e.getMessage());
        }
        return null;
    }
*/
    private String getEntryEditLink(Entry entry)
    {
        try {
            return  XPath.INSTANCE.valueOf(
                "atom:link[@rel='edit']/@href", entry);
        } catch (XPathException e) {
            fail("Unable to get edit link "+e.getMessage());
        }
        return null;
    }
    private String getEntryEditMediaLink(Entry entry)
    {
        try {
            return  XPath.INSTANCE.valueOf(
                "atom:link[@rel='edit-media']/@href", entry);
        } catch (XPathException e) {
            fail("Unable to get edit-media link "+e.getMessage());
        }
        return null;
    }

    public static <T extends Element> T loadElement(String path, Class<T> clazz)
        throws IOException
    {
        // find the jdo.properties in the class path
        InputStream inStream = Service.class.getResourceAsStream(path);
        if (inStream == null)
            throw new IOException("Resource not found: " + path);

        Document<T> element=Parser.INSTANCE.parse(inStream);
        return element.getRoot();
    }


    private static final Integer LOCATION=0;
    private static final Integer ENTRY=1;

    /**
     *
     * @param post
     * @param url
     * @param entry
     * @return location of new resource
     */
    private Map<Integer,Object> postEntry(Client client, String url, Entry entry)
    {
        Map<Integer,Object> reply=new HashMap<Integer,Object>();

        Response response=null;
        try
        {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            entry.getDocument().writeTo(out);
            ByteArrayRequestEntity are =
              new ByteArrayRequestEntity(
                out.toByteArray(),
                APPLICATION_ATOM_XML);

            response=client.post(url, are);
            assertEquals(201, response.getStatus());

            String location=response.getLocation();
            reply.put(LOCATION, location);
            log.debug(location);

            Document<Entry> responseEntry=response.getDocument();
            reply.put(ENTRY, responseEntry.getRoot());

        } catch (IOException e) {
            fail("Writing POST failed : "+e.getMessage());
        } catch (ParseException e) {
            log.info("Parsing POST reply failed",e);
        }
        finally {
            if (response!=null)
                response.release();
        }

        return reply;
    }

    private Map<Integer,Object> postImage(Client client, String url,
                                          InputStream is, String contentType)
    {
        Map<Integer,Object> reply=new HashMap<Integer,Object>();

        Response response=null;
        try
        {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            IOUtils.copy(is,out);
            is.close();

            ByteArrayRequestEntity are =
              new ByteArrayRequestEntity(
                out.toByteArray(),
                IMAGE_PNG);

            RequestOptions options=new RequestOptions();
            options.setContentType(IMAGE_PNG);
            response=client.post(url, are, options);
            assertEquals(201, response.getStatus());

            String location=response.getLocation();
            reply.put(LOCATION, location);
            log.debug(location);

            Document<Entry> responseEntry=response.getDocument();
            reply.put(ENTRY, responseEntry.getRoot());

        } catch (IOException e) {
            fail("Writing POST failed : "+e.getMessage());
        } catch (ParseException e) {
            log.info("Parsing POST reply failed",e);
        }
        finally {
            if (response!=null)
                response.release();
        }

        return reply;
    }

    private Service getService(Client client, String url)
    {
        Response response=null;
        try
        {
            response=client.get(url);
            assertEquals(200,response.getStatus());
            Document<Service> responseFeed=response.getDocument();
            return responseFeed.getRoot();
        } catch (ParseException e) {
            log.info("Parsing GET reply failed",e);
        }
        finally {
            if (response!=null)
                response.release();
        }

        return null;
    }


    private Feed getFeed(Client client, String url)
    {
        Response response=null;
        try
        {
            response=client.get(url);
            assertEquals(200,response.getStatus());
            Document<Feed> responseFeed=response.getDocument();
            return responseFeed.getRoot();
        } catch (ParseException e) {
            log.info("Parsing GET reply failed",e);
        }
        finally {
            if (response!=null)
                response.release();
        }

        return null;
    }

    private Entry getEntry(Client client, String url)
    {
        Response response=null;
        try
        {
            response=client.get(url);
            assertEquals(200,response.getStatus());
            Document<Entry> responseEntry=response.getDocument();
            return responseEntry.getRoot();
        } catch (ParseException e) {
            log.info("Parsing GET reply failed",e);
        }
        finally {
            if (response!=null)
                response.release();
        }

        return null;
    }

    private void deleteEntry(Client client, String url)
    {
        Response response=null;
        try
        {
            response=client.delete(url);
            assertEquals(200,response.getStatus());
        } catch (ParseException e) {
            log.info("Parsing GET reply failed",e);
        }
        finally {
            if (response!=null)
                response.release();
        }
    }

    private void putEntry(
        Client client, String url, Entry entry)
    {
        Response response=null;
        try
        {
            StringWriter pw=new StringWriter();
            entry.getDocument().writeTo(pw);
            StringRequestEntity sre=
                new StringRequestEntity(pw.toString(),APPLICATION_ATOM_XML, "UTF-8");
            RequestOptions options=new RequestOptions();
            options.setContentType(APPLICATION_ATOM_XML);
            response=client.put(url, sre, options);
            assertEquals(200,response.getStatus());
        } catch (IOException e) {
            fail("Writing PUT failed : "+e.getMessage());
        } catch (ParseException e) {
            log.info("Parsing PUT reply failed",e);
        }
        finally {
            if (response!=null)
                response.release();
        }
    }

    private void putImage(
        Client client, String url, InputStream is, String contentType)
    {
        Response response=null;
        try
        {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            IOUtils.copy(is,out);
            is.close();

            ByteArrayRequestEntity are =
                new ByteArrayRequestEntity(
                    out.toByteArray(),
                    IMAGE_PNG);

            RequestOptions options=new RequestOptions();
            options.setContentType(IMAGE_PNG);

            response=client.put(url, new ByteArrayRequestEntity(are.getContent()));
            assertEquals(200,response.getStatus());
        } catch (IOException e) {
            fail("Writing PUT failed : "+e.getMessage());
        } catch (ParseException e) {
            log.info("Parsing PUT reply failed",e);
        }
        finally {
            if (response!=null)
                response.release();
        }
    }

    private <T extends Element> void debugWrite(Document<T> document)
    {
        try {
            StringWriter sw=new StringWriter();
            document.writeTo(sw);
            sw.flush();
            log.debug(sw.toString());
        } catch (IOException e1) {
            log.error("Could not output document",e1);
        }
    }

}
